iT邦幫忙

2026 iThome 鐵人賽

0

今日目標

延續 Day 26 完成的會員註冊與登入功能,以及 Day 27 建立的預約掛號 API,今天開始整合智慧醫院管理平台的會員個人資料與我的預約功能。

先前已經使用 Angular 完成個人資料、我的預約、預約詳細及取消預約等頁面,但這些畫面最初主要使用固定的 Demo 資料,還沒有完整串接正式後端。

Day 27 已經完成 Oracle 預約資料表及 ASP.NET Core API,因此這次的重點不是重新設計畫面,而是將原本的展示頁面改成能透過 HTTP API 取得正式會員資料的功能。

同時,也希望讓會員可以修改自己的基本資料,並確保登入狀態、會員權限與畫面資料保持一致。

今日規劃

  • 確認現有 Oracle 資料表是否足以支援個人資料功能。
  • 建立 ASP.NET Core 個人資料查詢與修改 API。
  • 將 Angular 正式個人資料頁串接 API。
  • 實作個人資料編輯及表單驗證。
  • 修改姓名後同步更新 Header 顯示。
  • 將我的預約列表及詳細頁改用正式 API。
  • 串接取消預約功能。
  • 區分正式會員頁面與 Demo 展示頁面。
  • 完成前後端測試與瀏覽器操作驗收。

一、確認現有 Oracle 資料結構

1. Day 28 需要重新建立資料表嗎?

在開始個人資料 API 前,我先檢查 Day 26、Day 27 已經建立的資料表。

目前與這次功能有關的主要資料表包括:

資料表 用途
USERS 儲存會員帳號、手機及登入相關資訊
PATIENTS 儲存病人姓名,並關聯會員帳號
DOCTOR_SCHEDULES 儲存實際日期化門診診次
APPOINTMENTS 儲存會員預約紀錄

檢查後發現,現有 Schema 已經足以支援這次預定的個人資料功能,因此不需要再建立新的資料表。

其中,會員姓名儲存在:

PATIENTS.PATIENT_NAME

手機則儲存在:

USERS.PHONE

會員帳號則使用:

USERS.USERNAME

這次決定先開放修改姓名與手機,帳號僅提供查看,不開放修改。

至於生日、性別、身分證字號及地址等資料,目前並沒有對應的正式資料庫欄位,因此這次不額外增加。

這樣可以先完成最基本的個人資料管理,也避免為了配合原本 Demo 畫面而新增暫時不需要的個資欄位。

2. USERS 與 PATIENTS 的關聯

目前系統透過 PATIENTS.USER_ID 對應 USERS.USER_ID。

因此,會員登入後,後端可以根據已驗證的使用者身分,查詢對應的病人資料。

這次也特別確認,不能由前端自行提供 USER_ID 或 PATIENT_ID 來決定要修改哪一筆資料。

真正的會員身分必須由後端從登入狀態中取得,才能避免修改到其他會員的個人資料。


二、建立 ASP.NET Core 個人資料 API

完成資料庫結構確認後,開始實作個人資料 API。

Day 26 已經有:

GET /api/auth/me

這支 API 主要負責確認目前會員的登入狀態,並回傳會員身分資訊。

但原本沒有提供手機欄位,也沒有個人資料修改功能。

因此,這次新增獨立的 Profile API,讓身分驗證與個人資料管理有比較清楚的分工。

1. API 清單

HTTP API 功能
GET /api/profile 查詢目前登入會員的個人資料
PATCH /api/profile 修改目前登入會員的姓名與手機

這次新增的後端主要檔案:

Controllers/
└── ProfileController.cs

Dtos/
└── ProfileDtos.cs

Profile/
├── ProfileService.cs
├── ProfileValidation.cs
├── ProfileModels.cs
└── ProfileHttp.cs

Data/
└── ProfileRepository.cs

延續原本的 ASP.NET Core 架構,各個部分的職責如下:

  • Controller:接收 HTTP Request,確認登入及回傳結果。
  • DTO:定義 API 的輸入與輸出資料格式。
  • Service:處理個人資料查詢、修改與交易流程。
  • Validation:驗證姓名及手機格式。
  • Repository:使用參數化 SQL 存取 Oracle。
  • ProfileHttp:處理依賴注入及相關 HTTP 錯誤。

這樣可以避免把所有資料驗證及 SQL 操作直接寫在 Controller 裡。

2. GET /api/profile

首先實作個人資料查詢 API。

GET /api/profile

會員登入後,後端會根據已驗證的 Cookie 身分取得對應的 USER_ID,再查詢 USERS 及 PATIENTS。

回傳格式例如:

{
  "data": {
    "username": "profile01",
    "displayName": "虛構測試會員",
    "phone": null
  }
}

其中:

  • username:會員帳號。
  • displayName:會員姓名。
  • phone:手機號碼,可以為 null。

這支 API 只回傳畫面需要的個人資料,不包含密碼雜湊或其他登入憑證。

另外,也設定 Cache-Control: no-store,避免個人資料被不必要地快取。

3. PATCH /api/profile

接著實作修改個人資料 API。

PATCH /api/profile

Request 範例:

{
  "displayName": "虛構測試會員新姓名",
  "phone": "0912345678"
}

這次選擇使用 PATCH,主要是因為會員不一定每次都會修改全部欄位。

例如只修改姓名時,可以只傳送:

{
  "displayName": "新姓名"
}

如果想清空手機,可以傳送:

{
  "phone": null
}

這裡有一個需要特別注意的地方:

欄位沒有傳送,與欄位明確傳送 null,代表不同的意思。

Request 處理方式
沒有 displayName 姓名保持原值
沒有 phone 手機保持原值
phone: null 清空手機
displayName: null 拒絕修改
{} 不更新資料,回傳目前資料

因此,這次後端使用 JsonElement.ValueKind 區分欄位是否存在,避免沒有修改的資料被意外覆蓋。


三、個人資料驗證與資料庫交易

1. 姓名與手機驗證

個人資料修改不能直接將前端送來的內容寫進 Oracle,因此這次也加入欄位驗證。

欄位 驗證規則
姓名 必填,去除前後空白後為 1~100 個 Unicode 碼點
姓名內容 拒絕控制字元及無效 Unicode
手機 選填
手機格式 有值時必須為 09 開頭的 10 碼數字
清空手機 轉換為資料庫 NULL

手機格式使用:

^09[0-9]{8}$

因此不接受:

  • 不足或超過 10 碼。
  • 內部空白。
  • 連字號。
  • +886 格式。

這次前後端使用一致的驗證規則,減少使用者在前端輸入成功,但送到後端後才發現格式不符的情況。

2. 本人權限檢查

Profile API 延續 Day 26 的 Cookie Authentication。

使用者登入後,後端會確認 Cookie 的有效性及帳號狀態,再取得可信的會員 ID。

Repository 只使用後端取得的 USER_ID 查詢或修改資料。

前端不能透過 Request 指定其他人的 ID。

另外,PATCH 也沿用既有 CSRF 保護。

Angular 會先取得:

GET /api/auth/csrf

接著送出 PATCH 時,由現有 HttpClient XSRF 機制附加驗證資訊。

這次沒有重新建立另一套登入系統,而是直接沿用原本的身分驗證與 CSRF 設計。

3. 使用 Oracle Transaction

因為姓名與手機分別位於不同資料表,所以修改個人資料可能需要同時更新 USERS 與 PATIENTS。

這次使用同一個 Oracle Transaction 處理整個更新流程。

主要步驟:

  1. 確認目前登入的會員身分。
  2. 驗證要修改的欄位。
  3. 開始資料庫交易。
  4. 確認 USERS 與 PATIENTS 的關聯。
  5. 只更新 Request 中實際提供的欄位。
  6. 重新讀取更新後的個人資料。
  7. 全部成功後 Commit。
  8. 發生錯誤則 Rollback。

這樣可以避免姓名更新成功、手機更新卻失敗,導致兩張資料表出現不一致的情況。

另外,目前採用同一欄位最後提交者生效的方式,尚未加入 ETag 或版本衝突偵測。


四、Angular 正式個人資料頁面串接

完成後端 API 後,接著將原本的 Angular 正式會員頁面串接到 /api/profile。

1. 建立 Profile API Service

這次新增:

src/app/core/
├── profile-api.models.ts
├── profile-api.service.ts
└── profile-validation.ts

其中:

  • profile-api.models.ts:定義個人資料 API 的資料型別。
  • profile-api.service.ts:使用 Angular HttpClient 呼叫 API。
  • profile-validation.ts:提供前端表單驗證。

API 使用相對路徑:

/api/profile

並沿用原本的 Angular Proxy 設定,不需要另外建立一套 HTTP 連線方式。

正式會員資料也不會存入 localStorage 或 sessionStorage,避免在瀏覽器中留下不必要的個人資料。

2. 個人資料查看模式

會員進入正式 /profile 頁面後,Angular 會呼叫 GET API 載入個人資料。

畫面主要顯示:

  • 會員帳號。
  • 姓名。
  • 手機號碼。
  • 編輯個人資料按鈕。

其中帳號只能查看,姓名與手機可以進入編輯模式修改。

這次也補上載入中、載入失敗與重新載入等狀態,避免 API 還沒回應時,畫面直接出現不完整資料。

正式個人資料查看模式

https://ithelp.ithome.com.tw/upload/images/20261101/20178845hyNr4e5dx3.png

3. 個人資料編輯模式

這次使用 Angular Reactive Forms 處理姓名與手機輸入。

會員點擊「編輯」後,才會切換成可修改狀態。

編輯時提供:

  • 姓名輸入欄位。
  • 手機輸入欄位。
  • 儲存按鈕。
  • 取消按鈕。
  • 欄位驗證提示。

如果點擊取消,畫面會恢復原本載入的資料。

如果沒有實際修改內容,也不會送出不必要的 PATCH Request。

儲存期間則會停用重複提交,避免連續點擊造成多次更新。

正式個人資料編輯模

https://ithelp.ithome.com.tw/upload/images/20261101/20178845RLVCmt9Lbg.png

4. 修改成功後同步 Header 姓名

這次遇到另一個需要處理的問題:

假設會員在個人資料頁修改姓名,但網站 Header 仍然顯示登入時的舊姓名,就會造成畫面資料不一致。

因此,我在既有 AuthService 中加入:

syncProfileName()

當 Profile PATCH 成功後,會使用 API 回傳的姓名更新原本的登入狀態。

這樣 Header 就能同步顯示新姓名,不需要重新整理整個網站。

另外,也延續既有的 revision 機制,避免較早送出的 /api/auth/me 回應覆蓋剛修改完成的新姓名。

如果會員在請求尚未完成時登出,或切換成另一個會員,舊的 API 回應也不能把前一位會員的資料重新顯示出來。

這次讓我理解到,前端串接 API 不只是把資料顯示出來,也需要考慮多個非同步請求之間的狀態一致性。

Network 中 GET/PATCH Profile API 的請求與狀態碼

https://ithelp.ithome.com.tw/upload/images/20261101/20178845hknSn9nS70.png


五、Angular 我的預約列表串接

完成個人資料功能後,接著將原本的「我的預約」頁面改成正式 API 資料來源。

1. 從 Demo 資料改成正式 API

原本我的預約頁面使用 AppointmentDemoService 顯示固定的測試資料。

這樣雖然可以先完成 UI,但無法真正依據目前登入的會員顯示預約紀錄。

因此,這次新增:

src/app/core/
├── appointment-api.models.ts
└── appointment-api.service.ts

使用 Angular HttpClient 呼叫 Day 27 已完成的 API。

HTTP API 用途
GET /api/appointments/me 載入本人預約列表
GET /api/appointments/{id} 查詢預約詳細
POST /api/appointments/{id}/cancel 取消預約

正式列表不再使用 Demo Catalog 查詢醫師或科別,而是直接使用 API 回傳的醫師名稱、科別、診間與預約資訊。

日期時間則明確使用 Asia/Taipei 顯示,避免不同時區造成日期或時間錯誤。

2. 我的預約頁面

這次保留原本的預約卡片及篩選介面,主要修改資料來源與狀態處理。

正式頁面支援:

  • 載入本人預約紀錄。
  • 顯示預約狀態。
  • 預約日期與時間。
  • 醫師及科別。
  • 診間資訊。
  • 預約編號。
  • 空清單提示。
  • 錯誤訊息與重新載入。

另外,預約日期已經過去,不代表病人一定完成看診。

因此,這次沒有直接把過去的預約顯示成「已完成看診」,而是依照實際時間及後端狀態區分。

3. 使用真實 Oracle 測試預約紀錄

Day 27 真實 Oracle 測試完成後,資料庫保留了兩筆已取消的測試預約。

預約 ID 所屬會員 狀態
2 syy001 cancelled
3 syy001 cancelled

因此,這次也將這兩筆資料作為正式會員預約列表的驗收情境。

使用 syy001 登入時,應能查看自己的兩筆已取消預約。

使用 syy000 登入時,則不應顯示 syy001 的預約資料。

這部分除了測試列表顯示,也能協助確認登入會員切換後,前端不會繼續保留上一位會員的資料。

正式我的預約頁面,顯示兩筆已取消預約

https://ithelp.ithome.com.tw/upload/images/20261101/20178845O3rKbpg8Pc.png


六、預約詳細與取消功能串接

1. 預約詳細頁

這次將正式預約詳細頁改成依據 API 回傳資料顯示。

例如:

/appointments/3

Angular 會使用網址中的預約 ID,呼叫:

GET /api/appointments/3

取得該筆預約的詳細內容。

這次也處理直接輸入網址、重新整理及切換不同預約 ID 的情況。

如果預約不存在,或目前登入會員沒有權限查看,則顯示安全的錯誤訊息,不直接暴露其他會員的資料。

另外,正式 Oracle 的數字 ID 與原本 Demo 的預約 ID 是不同的,因此不會將 demo-1 之類的識別碼送到正式 API。

正式預約詳細頁面

https://ithelp.ithome.com.tw/upload/images/20261101/201788457fcs2TqvrU.png

2. 取消預約 Dialog

原本 Angular 已經有取消預約確認視窗,因此這次沒有重新設計整個 Dialog,而是保留原本的 UI 與焦點管理。

當會員確認取消時,前端會呼叫:

POST /api/appointments/{id}/cancel

主要流程如下:

  1. 確認目前預約可以取消。
  2. 開啟取消確認視窗。
  3. 會員點擊確認。
  4. 取得 CSRF 驗證資訊。
  5. 呼叫取消 API。
  6. 等待後端回應。
  7. 成功後更新畫面上的預約狀態。

這次特別注意,不能在後端尚未回傳成功前,就直接將預約顯示為已取消。

如果 API 失敗,原本的預約資料仍應保留,並顯示適當的錯誤提示。

同時,也加入儲存中的防重複點擊處理。

由於 Day 27 現有兩筆測試預約都已經取消,因此這次正式瀏覽器測試主要可以確認已取消狀態與詳細資料顯示;取消 Dialog 的完整成功流程則已有 Angular Mock 測試驗證。


七、正式頁面與 Demo 頁面分離

這次整合時,還有一個需要處理的問題:原本的預約頁面已經有完整 Demo 功能,如果直接改成正式 API,可能會影響之前展示及測試使用的頁面。

因此,這次決定保留 Demo 功能,但將正式會員頁面與 Demo 路由分開。

正式頁面 Demo 頁面
/profile /demo/profile
/appointments /demo/appointments
/appointments/:id /demo/appointments/:id

正式頁面使用後端 API,Demo 頁面則繼續保留固定展示資料。

另外,正式 API 發生錯誤時,也不會自動改用 Demo 資料代替。

這樣可以避免使用者以為自己看到的是正式預約紀錄,實際上卻是固定的展示內容。

同時,我也保留原本的 HTML、CSS 及 RWD 設計,僅針對 API 綁定、載入狀態、錯誤提示與必要的按鈕樣式進行調整。


八、登入狀態與前端資料安全

因為個人資料及預約紀錄都屬於會員專屬資訊,所以這次也加強前端登入狀態與資料清除的處理。

1. 加入 Auth Guard

正式個人資料、我的預約與預約詳細頁都需要登入後才能進入。

因此,延續既有的 authGuard,當使用者尚未登入時,會引導至登入頁面。

但這只是前端導覽保護,真正的會員權限仍然由 ASP.NET Core API 負責驗證。

2. 登出後清除會員資料

當會員登出時,前端需要清除原本載入的個人資料與預約狀態。

如果另一位會員登入,不能直接看到前一位會員的預約紀錄。

因此,這次也加入登入工作階段與請求版本檢查,避免較晚完成的 HTTP 回應覆蓋目前的新會員狀態。

3. 不將正式個資存入瀏覽器儲存空間

這次正式個人資料與預約資料主要保留在 Angular 的記憶體狀態中。

不使用 localStorage 或 sessionStorage 保存正式會員的個人資料及預約紀錄。

這讓資料來源與登入狀態的管理更清楚,也減少不必要的瀏覽器端資料殘留。


九、測試與驗證

1. ASP.NET Core Profile Mock 測試

完成後端個人資料 API 後,先使用 Mock Repository 與資料庫交易替身測試。

測試內容包括:

  • 會員本人資料查詢。
  • 未登入拒絕存取。
  • CSRF 驗證。
  • 修改姓名及手機。
  • 清空手機。
  • 省略欄位時保留原值。
  • 不合法資料拒絕。
  • 不允許修改其他會員資料。
  • Transaction 失敗時 Rollback。
  • API 錯誤處理。

測試結果:

測試項目 結果
Day 28 Profile Mock 533/533 通過
Day 26 Auth Mock 回歸 304/304 通過
Day 27 Booking Mock 回歸 286/286 通過
合計 1,123/1,123 通過

這些測試主要驗證後端邏輯,並使用替身模擬資料庫行為,不應與真實 Oracle 更新測試混為一談。

2. Angular 單元測試

完成 Angular 個人資料及預約功能後,也新增相關測試。

主要涵蓋:

  • 個人資料載入及修改。
  • 姓名與手機驗證。
  • Header 姓名同步。
  • 登出後資料清除。
  • 預約列表與詳細載入。
  • 空清單及錯誤處理。
  • 取消預約成功與失敗。
  • CSRF 流程。
  • 避免重複送出。
  • 避免舊的 API 回應覆蓋新狀態。
  • 正式及 Demo 頁面的功能回歸。

最後測試結果:

項目 結果
Angular 單元測試 274/274 通過
TypeScript 型別檢查 通過
Production Build 成功
建置警告與錯誤 0

另外也完成相關 axe 自動化可及性檢查。

https://ithelp.ithome.com.tw/upload/images/20261101/20178845jzWtP44LuZ.pngAngular 274/274 單元測試結果

3. 瀏覽器實際操作驗收

完成程式測試後,我再透過瀏覽器確認正式頁面的操作流程。

本次驗收主要包含:

  • 正式會員登入及頁面存取。
  • 個人資料查看與編輯。
  • 個人資料重新整理。
  • 我的預約列表。
  • 已取消預約的詳細資訊。
  • 不同會員的資料隔離。
  • 正式與 Demo 頁面分離。
  • 登出後的資料清除。
  • 手機版 RWD 排列。

這次也確認,正式頁面不應再直接使用原本固定的 Demo 預約資料。

其中,Day 27 已完成真實 Oracle 預約 API 整合測試,因此可以使用已保留的取消歷史作為 Day 28 的預約列表資料來源。

Profile 的真實 Oracle 修改結果則應以實際 PATCH、GET 讀回及資料庫查詢紀錄為準;Mock 測試通過不等於已證明所有資料庫寫入情境。


十、今日遇到的問題與學習

1. 正式 API 與 Demo Service 不能混用

原本已完成的 Angular 頁面使用固定 Demo 資料,適合先進行 UI 設計與測試。

但正式會員功能必須使用真正的登入身分及後端資料來源。

因此,這次將正式與 Demo 路由分開,保留原本展示功能,也避免資料來源混淆。

2. PATCH 必須區分欄位省略與 null

一開始容易認為只要把表單所有欄位送給後端就好,但實際上使用者可能只修改其中一個欄位。

因此,這次特別處理欄位省略、清空與保持原值的差別,避免沒有修改的資料被意外覆蓋。

3. Header 與個人資料頁需要同步

會員修改姓名後,Header 不能一直顯示舊資料。

所以這次透過既有 AuthService 更新登入狀態,並保留 revision 保護,處理非同步 API 回應的順序問題。

4. 取消預約必須等待後端確認

前端不能因為使用者按下取消,就直接假設 Oracle 已成功更新。

這次改成等待 HTTP 成功後才更新畫面,失敗則保留原本資料,避免前後端狀態不一致。

5. API 測試與畫面測試是不同的

後端 Mock 通過、Angular 單元測試通過,代表程式邏輯已經通過對應的自動化測試。

但實際使用者操作還涉及瀏覽器、HTTP Proxy、Cookie、畫面排版與資料庫讀寫,因此仍需要另外進行整合驗證。

這次也讓我更理解前後端分離架構中,每一層測試各自負責的範圍。


十一、今日完成

  • 確認現有 Oracle 資料表可支援個人資料功能,不需新增資料表。
  • 完成 ASP.NET Core GET /api/profile。
  • 完成 ASP.NET Core PATCH /api/profile。
  • 加入姓名、手機欄位驗證。
  • 加入會員本人權限與 CSRF 驗證。
  • 使用 Oracle Transaction 處理跨資料表更新。
  • 完成 Angular 正式個人資料查看與編輯功能。
  • 完成個人資料修改後 Header 姓名同步。
  • 完成 Angular 我的預約列表 API 串接。
  • 完成預約詳細 API 串接。
  • 完成取消預約 Dialog 的 API 串接。
  • 將正式會員頁面與 Demo 展示頁面分離。
  • 完成後端 Profile Mock 533 項測試。
  • 完成 Day 26、Day 27 後端 Mock 回歸,共計 1,123 項通過。
  • 完成 Angular 274 項單元測試。
  • 完成 TypeScript 檢查及 Production Build。
  • 進行正式會員頁面與 RWD 瀏覽器操作驗收。

十二、下一步

Day 28 已經完成個人資料與我的預約相關的前後端功能整合。

接下來將繼續開發正式預約掛號頁面,讓會員不只可以查看自己的預約,也能直接從醫師詳細頁選擇實際日期化診次,並透過正式 API 建立預約。

下一階段預計包含:

  • 將正式醫師詳細頁導向正式預約流程。
  • 使用 /api/schedules 顯示 Oracle 實際診次。
  • 顯示可預約狀態及剩餘名額。
  • 串接 POST /api/appointments。
  • 完成正式預約成功頁。
  • 將正式掛號結果與「我的預約」串成完整流程。

這次最大的收穫是,將原本使用固定 Demo 資料的 Angular 頁面,逐步改成透過 ASP.NET Core API 取得正式會員資料,並進一步理解了會員權限、表單驗證、非同步狀態同步及前後端資料一致性的重要性。


上一篇
Day 27|預約掛號 API與 Oracle 整合測試
下一篇
Day 29|完整系統整合測試
系列文
從 Figma 到 Angular:打造智慧醫院預約掛號系統的 30 天實作之旅 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言